﻿using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace Morus.Presentation
{
    public class MessageCoordinator : IMessageCoordinator
    {
        private readonly ConcurrentDictionary<Type, IList> _messages;
        private readonly ConcurrentDictionary<Type, IList<Action<object>>> _messageReceivedCallbacks;
        private readonly ConcurrentDictionary<Type, IList<Action>> _neverReceivedCallbacks;

        private bool _closed;
        private readonly object _closeLock = new object();

        [DebuggerStepThrough]
        public MessageCoordinator()
        {
            _messages = new ConcurrentDictionary<Type, IList>();
            _messageReceivedCallbacks = new ConcurrentDictionary<Type, IList<Action<object>>>();
            _neverReceivedCallbacks = new ConcurrentDictionary<Type, IList<Action>>();
        }

        public void Publish<TMessage>(TMessage message)
        {
            if (_closed)
            {
                throw new InvalidOperationException("Messages can't be published or subscribed to after the message bus has been closed. In a typical page lifecycle, this happens during PreRenderComplete.");
            }

            AddMessage(message);
            PushMessage(message);
        }

        private void AddMessage<TMessage>(TMessage message)
        {
            var messageList = _messages.GetOrAdd(typeof(TMessage), (t) => new List<TMessage>());

            lock (messageList)
            {
                messageList.Add(message);
            }
        }

        private void PushMessage<TMessage>(TMessage message)
        {
            var messageType = typeof(TMessage);

            var callbackTypes = _messageReceivedCallbacks.Keys.Where(k => k.IsAssignableFrom(messageType));

            var callbacks = callbackTypes.SelectMany(t => _messageReceivedCallbacks[t]);

            foreach (var callback in callbacks)
            {
                callback(message);
            }
        }

        public void Subscribe<TMessage>(Action<TMessage> messageReceivedCallback)
        {
            Subscribe(messageReceivedCallback, null);
        }

        public void Subscribe<TMessage>(Action<TMessage> messageReceivedCallback, Action neverReceivedCallback)
        {
            if (_closed)
            {
                throw new InvalidOperationException("Messages can't be published or subscribed to after the message bus has been closed. In a typical page lifecycle, this happens during PreRenderComplete.");
            }

            Check.Argument.IsNotNull(messageReceivedCallback, "messageReceivedCallback");

            AddMessageReceivedCallback(messageReceivedCallback);
            AddNeverReceivedCallback<TMessage>(neverReceivedCallback);
            PushPreviousMessages(messageReceivedCallback);
        }

        private void AddMessageReceivedCallback<TMessage>(Action<TMessage> messageReceivedCallback)
        {
            var intermediateReceivedCallback = new Action<object>(m => messageReceivedCallback((TMessage)m));

            var receivedList = _messageReceivedCallbacks.GetOrAdd(typeof(TMessage), (t) => new List<Action<object>>());

            lock (receivedList)
            {
                receivedList.Add(intermediateReceivedCallback);
            }
        }

        private void AddNeverReceivedCallback<TMessage>(Action neverReceivedCallback)
        {
            if (neverReceivedCallback == null)
            {
                return;
            }

            var neverReceivedList = _neverReceivedCallbacks.GetOrAdd(typeof(TMessage), (t) => new List<Action>());

            lock (neverReceivedList)
            {
                neverReceivedList.Add(neverReceivedCallback);
            }
        }

        private void PushPreviousMessages<TMessage>(Action<TMessage> messageReceivedCallback)
        {
            var previousMessageTypes = _messages.Keys.Where(mt => typeof(TMessage).IsAssignableFrom(mt));

            var previousMessages = previousMessageTypes.SelectMany(t => _messages[t].Cast<TMessage>()).ToArray();

            foreach (var previousMessage in previousMessages)
            {
                messageReceivedCallback(previousMessage);
            }
        }

        public void Close()
        {
            if (_closed)
            {
                return;
            }

            lock (_closeLock)
            {
                if (_closed)
                {
                    return;
                }
                _closed = true;
            }

            FireNeverReceivedCallbacks();
        }

        private void FireNeverReceivedCallbacks()
        {
            var neverReceivedMessageTypes = _neverReceivedCallbacks.Keys.Where(neverReceivedMessageType =>
                !_messages.Keys.Any(neverReceivedMessageType.IsAssignableFrom));

            var callbacks = neverReceivedMessageTypes.SelectMany(t => _neverReceivedCallbacks[t]);

            foreach (var callback in callbacks)
            {
                callback();
            }
        }
    }
}